/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.web.misc.cg.extractor.impl;

import com.google.common.collect.Lists;
import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.Type;
import org.vacoor.nothing.common.util.StringUtils;
import org.vacoor.nothing.web.misc.cg.descriptor.*;
import org.vacoor.nothing.web.misc.cg.extractor.MappingDescriptorExtractor;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Java Doc 映射注解描述符提取器
 *
 * @author vacoor
 */
public class JavaMappingDescriptorExtractor implements MappingDescriptorExtractor, JavaMappingAnnotation {
    Map<JavaClass, MappedTable> localCache = new HashMap<JavaClass, MappedTable>();

    @Override
    public List<MappedTable> execute(File[] sourceDirs) throws FileNotFoundException {
        List<MappedTable> mappedTables = Lists.newArrayList();
        JavaDocBuilder builder = new JavaDocBuilder();
        for (File dir : sourceDirs) {
            if (!dir.exists()) {
                throw new FileNotFoundException(dir.getAbsolutePath());
            }
            builder.addSourceTree(dir);
        }
        JavaClass[] classes = builder.getClasses();
        for (JavaClass clazz : classes) {
            DocletTag mapped = clazz.getTagByName(TABLE, true);
            if (null != mapped) {
                MappedTable mappedTable = extractTableDescriptor(clazz);
                if (null != mappedTable) {
                    mappedTables.add(mappedTable);
                }
            }
        }
        return mappedTables;
    }

    /**
     * 提取给定类的表描述符
     *
     * @param clazz
     * @return
     */
    public MappedTable extractTableDescriptor(JavaClass clazz) {
        MappedTable table = localCache.get(clazz);
        if (null != table) {
            return table;
        }
        // 缓存中不存在
        table = new MappedTable();
        table.setTypeName(clazz.getName());
        table.setFullTypeName(clazz.getFullyQualifiedName());
        localCache.put(clazz, table);   // 防止死循环

        DocletTag tag = clazz.getTagByName(TABLE, true);
        if (null == tag) {
            throw new IllegalStateException("can't found @" + TABLE + " on " + table.getFullTypeName());
        } else {
            String tname = tag.getValue();
            table.setTable(nvl(tname, "T_" + clazz.getName()));
        }

        Map<String, JavaField> fields = extractTagedField(clazz);
        for (Map.Entry<String, JavaField> fieldEntry : fields.entrySet()) {
            JavaField field = fieldEntry.getValue();
            Map<String, String> paramMap;

            DocletTag oneToOneTag = field.getTagByName(ONE_TO_ONE);
            DocletTag oneToManyTag = field.getTagByName(ONE_TO_MANY);
            DocletTag manyToOneTag = field.getTagByName(MANY_TO_ONE);
            DocletTag manyToManyTag = field.getTagByName(MANY_TO_MANY);
            DocletTag idTag = field.getTagByName(ID);
            DocletTag colTag = field.getTagByName(COLUMN);
            String source = field.getParentClass().getFullyQualifiedName();

            if (null != oneToOneTag) {
                OneToOne oneToOne = new OneToOne(source);
                table.addOneToOne(oneToOne);
                oneToOne.setRel(
                        extractRelationship(oneToOne, field, oneToOneTag)
                );

                continue;
            }

            if (null != oneToManyTag) {
                OneToMany oneToMany = new OneToMany(source);
                table.addOneToMany(oneToMany);
                oneToMany.setRel(
                        extractRelationship(oneToMany, field, oneToManyTag)
                );

                continue;
            }

            if (null != manyToOneTag) {
                ManyToOne manyToOne = new ManyToOne(source);
                table.addManyToOne(manyToOne);
                manyToOne.setRel(
                        extractRelationship(manyToOne, field, manyToOneTag)
                );

                continue;
            }

            if (null != manyToManyTag) {
                ManyToMany manyToMany = new ManyToMany(source);
                table.addManyToMany(manyToMany);
                manyToMany.setRel(
                        extractRelationship(manyToMany, field, manyToManyTag)
                );

                continue;
            }

            if (null != colTag || null != idTag) {
                MappedColumn col = new MappedColumn();
                col.setProperty(field.getName());
                // col.setJavaFullTypeName(field.getType().getFullyQualifiedName());
                JavaClass jClazz = field.getType().getJavaClass();
                col.setJavaFullTypeName(jClazz.getFullyQualifiedName());
                col.setJavaTypeName(jClazz.getName());

                if (null != colTag) {
                    paramMap = colTag.getNamedParameterMap();
                    String value = paramMap.get(COLUMN_NAME);
                    col.setColumn(StringUtils.camelCaseToUnderscore(nvl(value, field.getName()), true));

                    value = paramMap.get(COLUMN_UNIQUE);
                    col.setUnique(nvl(value, false));

                    value = paramMap.get(COLUMN_NULLABLE);
                    col.setNullable(nvl(value, true));

                    value = paramMap.get(COLUMN_INSERTABLE);
                    col.setInsertable(nvl(value, true));

                    value = paramMap.get(COLUMN_UPDATABLE);
                    col.setUpdatable(nvl(value, true));

                    value = paramMap.get(COLUMN_LENGTH);
                    col.setLength(nvl(value, 255));

                    value = paramMap.get(COLUMN_SCALE);
                    col.setScale(nvl(value, 0));
                } else {
                    col.setColumn(field.getName());
                }
                if (null != idTag) {
                    table.addId(col);
                } else {
                    table.addColumn(col);
                }
            }
        }

        return table;
    }

    protected MappedTable extractRelationship(Relationship rel, JavaField field, DocletTag tag) {
        rel.setProperty(field.getName());
        MappedTable table;
        if (rel instanceof ToOne) {
            table = extractTableDescriptor(field.getType().getJavaClass());
        } else {
            Type[] types = field.getType().getActualTypeArguments();
            if (null == types || 1 > types.length) {
                throw new RuntimeException("can not find filed " + field.getDeclarationSignature(true) + "'s component type");
            }
            JavaClass cmpType = types[0].getJavaClass();
            table = extractTableDescriptor(cmpType);
        }

        if (null != tag) {
            Map<String, String> paramMap = tag.getNamedParameterMap();
            String value = paramMap.get(REL_FETCH);
            rel.setFetch(REL_FETCH_EAGER.equals(value));
            if (!StringUtils.hasText(value) && rel instanceof ToOne) {
                rel.setFetch(true);
            }


            value = paramMap.get(REL_OPTIONAL);
            // value = null == value || "".equals(value) ? "true" : value;
            rel.setOptional(nvl(value, false)); // 默认都是必选

            String mappedBy = paramMap.get(REL_MAPPED_BY);
            rel.setMappedBy(mappedBy);

            value = paramMap.get(REL_JOIN_COLUMN);
            String joinName = rel instanceof ToMany ? field.getParentClass().getName() : field.getName();
            joinName = StringUtils.camelCaseToUnderscore(joinName + "Id", true);
            rel.setJoinColumn(nvl(value, joinName));

            if (rel instanceof ManyToMany) {
                JoinTable joinTable = new JoinTable();
                ((ManyToMany) rel).setJoinTable(joinTable);
                DocletTag joinTableTag = field.getTagByName(REL_JOIN_TABLE);
                if (null != joinTableTag) {
                    Map<String, String> params = joinTableTag.getNamedParameterMap();
                    joinTable.setName(StringUtils.camelCaseToUnderscore(params.get("name"), true));
                    joinTable.setJoinColumn(StringUtils.camelCaseToUnderscore(params.get(REL_JOIN_COLUMN), true));
                    joinTable.setInverseJoinColumn(StringUtils.camelCaseToUnderscore(params.get(REL_INVERSE_JOIN_COLUMN), true));
                }
                if (!StringUtils.hasText(joinTable.getName())) {
                    joinTable.setName(StringUtils.camelCaseToUnderscore("t_" + StringUtils.uncapitalize(field.getParentClass().getName()) + "_" + field.getName(), true));
                }
                if (!StringUtils.hasText(joinTable.getJoinColumn())) {
                    joinTable.setJoinColumn(StringUtils.camelCaseToUnderscore(field.getParentClass().getName() + "_id", true));
                }
                if (!StringUtils.hasText(joinTable.getInverseJoinColumn())) {
                    joinTable.setInverseJoinColumn(StringUtils.camelCaseToUnderscore(field.getName() + "_id", true));
                }
            }
        }

        return table;
    }

    /**
     * 提取包含映射 Java Doc 的字段
     *
     * @param clazz
     * @return
     */
    protected Map<String, JavaField> extractTagedField(JavaClass clazz) {
        Map<String, JavaField> tagMap;
        JavaClass parent = clazz.getSuperJavaClass();
        JavaClass objClazz = clazz.getJavaClassContext().getClassByName(Object.class.getName());

        tagMap = null != parent && objClazz != parent ? extractTagedField(parent) : new LinkedHashMap<String, JavaField>();
        for (JavaField f : clazz.getFields()) {
            if (null != f.getTagByName(ID) ||
                    null != f.getTagByName(COLUMN) ||
                    null != f.getTagByName(ONE_TO_ONE) ||
                    null != f.getTagByName(MANY_TO_ONE) ||
                    null != f.getTagByName(ONE_TO_MANY) ||
                    null != f.getTagByName(MANY_TO_MANY)
                    ) {
                tagMap.put(f.getName(), f);
            }
        }
        return tagMap;
    }

    /* **************************************
     *
     * ************************************ */

    String nvl(String value, String defaultValue) {
        return null != value && !"".equals(value) ? value : defaultValue;
    }

    boolean nvl(String value, boolean def) {
        try {
            def = Boolean.parseBoolean(value);
        } catch (Exception ignore) {
        }
        return def;
    }

    int nvl(String value, int def) {
        try {
            def = Integer.parseInt(value);
        } catch (NumberFormatException ignore) {
        }
        return def;
    }
}
